import numpy as np
import pandas as pd


class backtest_minutes:
	"""
	opencost, closecost, closecost2 are all numeric scalar
	output is a value calculate by simple interest
	"""
	def __init__(self, TimestampSignal, TimestampPrice, opencost, closecost,
		closecost2):
		self.TimestampSignal = TimestampSignal
		self.TimestampPrice = TimestampPrice
		self.opencost = opencost
		self.closecost = closecost
		self.closecost2 = closecost2

	def long(self, openprice, closeprice, opencost, closecost):
		"""calculate the returns from long operations
		given open price, close price, cost of open the position, cost for close the position
		"""
		return ((closeprice * (1 - closecost)) / (
			openprice * (1 + opencost)) - 1)

	def short(self, openprice, closeprice, opencost, closecost):
		"""
		calculate the returns from short operations
		given open price, close price, cost of open the position, cost for close the position
		"""
		nominator = openprice * (1 - opencost) - closeprice * (1 + closecost)
		denominator = openprice * (1 - opencost)
		return (nominator / denominator)

	def crossday_long_with_stoploss(self, r, DTtradep, opencost, closecost,
		closecost2, openprice='close', closeprice='close'):
		"""
		r is the stoploss criteria, r is the loss from opening point
		DTtradep is a pandas dataframe, stands for datetime and trading price
		the first column of DTtradep is a 'DATETIME', which is a timestamp object
		the second column s a 'close', which is a float64 object
		opencost is the cost for open
		closecost is the normal close cost
		closecost2 is the intraday cost
		"""

		"""
		crossday_long_with_stoploss and crossday_short_with_stoploss
		tries to calculate the returns for any time (intraday or not)
		e.g. the returns between '2010-09-10 13:47:00' and '2010-09-20 09:56:00'
		if stoploss happens, then we use the next minutes bar as the price for closing the position
		if the stoploss did not happen, then we use the last element of tradep as
		the price for closing the position
		"""

		"""
		the function returns two values in list form
		(1) a dataframe with first column being datetime.date, it can be duplicated.
		as many transactions can be made on a single day.
		e.g. many times of stoploss, close the position at a day open. reopen in the mid of the day
		second column being returns of a set of these transactions
		(2) the second value is the ending datetime
		"""
		minr = np.where(DTtradep['low'] < DTtradep[openprice].iloc[0] * (1 - r))[0]
		tradingvalue = []
		if len(minr) != 0:  # stoploss occured
			# get the rows before the stoploss time
			interprice = DTtradep[:(minr[0] + 1)]
			interdates = interprice['Date'].unique()
			print("long stoploss occured! " + str(
				interprice['DATETIME'].iloc[-1]) + "\n")
			STOPLOSS = True
		else:  # if no stoploss happens
			interprice = DTtradep
			interdates = interprice['Date'].unique()
			STOPLOSS = False

		# if this is a intraday trade
		if (len(interdates) < 2):
			if STOPLOSS == True:
				tradingvalue.append(-r)
			else:
				tradingvalue.append(self.long(interprice[openprice].iloc[0],
				                              interprice[closeprice].iloc[-1],
				                              opencost, closecost2))

		# if this is not a intraday trade
		if (len(interdates) >= 2):
			# get the first day of trading return
			# because the cost of opening, we have to do that seperate
			closeprev = interprice[openprice].iloc[0]
			closethatday = interprice['close'][
				interprice['Date'] == interdates[0]].as_matrix()[-1]
			tradingvalue.append(self.long(closeprev, closethatday, opencost, 0))

			# if tradingvalue[0]<0:
			#    opennextday=interprice['close'][interprice['Date']==interdates[1]].as_matrix()[0]
			#    tradingvalue.append(self.long(closethatday,opennextday,0,closecost))
			#    datevalues=pd.DataFrame([interdates[:2],tradingvalue]).transpose()
			#    datevalues.columns=['Date','Returns']
			#    endperiod=interprice['DATETIME'][interprice['Date']==interdates[1]].iloc[0]-datetime.timedelta(minutes=1)
			#    return [datevalues,endperiod]

			for i in range(1, len(interdates) - 1):
				#  get the close price previous day
				closeprev = interprice['close'][
					interprice['Date'] == interdates[(i - 1)]].as_matrix()[-1]
				# get the close price today
				closethatday = interprice['close'][
					interprice['Date'] == interdates[i]].as_matrix()[-1]
				tradingvalue.append(self.long(closeprev, closethatday, 0, 0))

			if STOPLOSS == True:
				closeprev = interprice['close'][
					interprice['Date'] == interdates[
						len(interdates) - 2]].as_matrix()[0]
				closethatday = interprice[openprice].as_matrix()[0] * (1 - r)
				tradingvalue.append(
					self.long(closeprev, closethatday, 0, closecost))
			else:
				# get the last day of trading return
				# because the cost of closing, we have to do that seperate
				closeprev = interprice['close'][
					interprice['Date'] == interdates[
						len(interdates) - 2]].as_matrix()[-1]
				closethatday = interprice[closeprice].as_matrix()[-1]
				tradingvalue.append(
					self.long(closeprev, closethatday, 0, closecost))

		datevalues = pd.DataFrame([interdates, tradingvalue]).transpose()
		datevalues.columns = ['Date', 'Returns']

		endperiod = interprice['DATETIME'].iloc[-1]

		return [datevalues, endperiod]

	def crossday_short_with_stoploss(self, r, DTtradep, opencost, closecost,
		closecost2, openprice='close', closeprice='close'):
		maxr = np.where(DTtradep['high'] > DTtradep['close'].iloc[0] * (1 + r))[0]
		tradingvalue = []
		if len(maxr) != 0:  # stoploss occured
			# get the rows before the stoploss time
			interprice = DTtradep[:(maxr[0] + 1)]
			interdates = interprice['Date'].unique()
			print("short stoploss occured! " + str(
				interprice['DATETIME'].iloc[-1]) + "\n")
			STOPLOSS = True
		else:  # if no stoploss happens
			interprice = DTtradep
			interdates = interprice['Date'].unique()
			STOPLOSS = False

		# if this is a intraday trade
		if (len(interdates) < 2):
			if STOPLOSS == True:
				tradingvalue.append(-r)
			else:
				tradingvalue.append(self.short(interprice[openprice].iloc[0],
				                               interprice[closeprice].iloc[-1], opencost, closecost2))

		# if this is not a intraday trade
		if (len(interdates) >= 2):
			# get the first day of trading return
			# because the cost of opening, we have to do that seperate
			closeprev = interprice[openprice].iloc[0]
			closethatday = interprice['close'][interprice['Date'] == interdates[0]].as_matrix()[-1]
			tradingvalue.append(self.short(closeprev, closethatday, opencost, 0))

			# if tradingvalue[0]<0:
			#    opennextday=interprice['close'][interprice['Date']==interdates[1]].as_matrix()[0]
			#    tradingvalue.append(self.short(closethatday,opennextday,0,closecost))
			#    datevalues=pd.DataFrame([interdates[:2],tradingvalue]).transpose()
			#    datevalues.columns=['Date','Returns']
			#    endperiod=interprice['DATETIME'][interprice['Date']==interdates[1]].iloc[0]-datetime.timedelta(minutes=1)
			#    return [datevalues,endperiod]

			for i in range(1, len(interdates) - 1):
				#  get the close price previous day
				closeprev = interprice['close'][interprice['Date'] == interdates[(i - 1)]].as_matrix()[-1]
				# get the close price today
				closethatday = interprice['close'][interprice['Date'] == interdates[i]].as_matrix()[-1]
				tradingvalue.append(self.short(closeprev, closethatday, 0, 0))

			if STOPLOSS == True:
				closeprev = interprice['close'][interprice['Date'] == interdates[len(interdates) - 2]].as_matrix()[0]
				closethatday = interprice[openprice].as_matrix()[0] * (1 + r)
				tradingvalue.append(self.short(closeprev, closethatday, 0, closecost))
			else:
				# get the last day of trading return
				# because the cost of closing, we have to do that seperate
				closeprev = interprice['close'][interprice['Date'] == interdates[len(interdates) - 2]].as_matrix()[-1]
				closethatday = interprice[closeprice].as_matrix()[-1]
				tradingvalue.append(self.short(closeprev, closethatday, 0, closecost))

		datevalues = pd.DataFrame([interdates, tradingvalue]).transpose()
		datevalues.columns = ['Date', 'Returns']

		endperiod = interprice['DATETIME'].iloc[-1]

		return [datevalues, endperiod]

	def trading_index_finder(self, actions):
		"""
		find the first set of transaction: buy and then sell, or short and then cover
		i.e. find the first example of pair 1,-1 or -1,1
		if either actionindex1=='end' or actionindex2=='end', then we shouldn't preceed forward
		action is a munpy array or a pandas series
		"""
		# get the index for the first nonzero
		try:
			actionindex1 = np.where(actions != 0)[0][0]
		except:
			actionindex1 = 'end'
			actionindex2 = 'end'

		try:
			if (actions.iloc[actionindex1] == 1):
				actionindex2 = np.where(actions == -1)[0]
				actionindex2 = actionindex2[actionindex2 > actionindex1][0]

			if (actions.iloc[actionindex1] == -1):
				actionindex2 = np.where(actions == 1)[0]
				actionindex2 = actionindex2[actionindex2 > actionindex1][0]
		except:
			actionindex2 = 'end'

		return [actionindex1, actionindex2]

	def get_quantile_by_time(self, KnownState, quantile1=25, quantile2=75):
		"""
		input, KnownState is a dataframe with four columns
		DATETIME(pd.timestamp), Date(datetime.date), Time(datetime.time), Prediction(float64)
		find the 25 75 quantile for a set of prediction
		"""
		Qall = []
		TimestampLevels = KnownState['Time'].unique()
		for Tstamp in TimestampLevels:
			Qall.append(np.percentile(
				KnownState['Prediction'][KnownState['Time'] == Tstamp],
				[quantile1, quantile2]).reshape(1, 2))
		return np.concatenate(Qall)

	def actionlist_generator(self, startn=13, lookbackn=None, Timeeffect=True):
		"""
		input, TimestampSignal is a dataframe with four columns
		DATETIME(pd.timestamp), Date(datetime.date), Time(datetime.time), Prediction(float64)
		startn is the days that we get started
		generate an actionlist of 1(buy,cover), 0(maintain the position before), -1(short,sell)
		for example, if we are standing at 2007-07-05, then we using all the data backing to the first day of our prediction
		and then we use all the Predictions between the first day and 2007-07-04 to calculate a 25 75 quantile
		and then we examine if the 2007-07-05 is greater than the upper 25 quantile, if yes, write 1
		similiarly ,if the 2007-07-05 is smaller than the lower 25 quantile, if yes, write -1
		the output is DATETIME(pd.timestamp), Date(datetime.date), Time(datetime.time), Prediction(1,0,-1)
		"""
		self.actionlist = []
		stepsize = round(self.TimestampSignal.shape[0] / len(
			self.TimestampSignal['Date'].unique()))

		# remove the Date that has different stepsize
		if Timeeffect == True:
			for Date in self.TimestampSignal['Date'].unique():
				if sum(self.TimestampSignal['Date'] == Date) != stepsize:
					self.TimestampSignal = self.TimestampSignal[self.TimestampSignal['Date'] != Date]
					print(str(Date) + " has been removed because of missing data(prediction)!\n")

		startpoint = startn * stepsize
		endpoint = int(
			self.TimestampSignal.shape[0] / stepsize) * stepsize - stepsize
		for i in range(startpoint, (endpoint + 1), stepsize):
			if lookbackn == None:
				KnownState = self.TimestampSignal[:i]
			else:
				lookbackn = lookbackn * stepsize
				KnownState = self.TimestampSignal[(i - lookbackn):i]

			Qall = self.get_quantile_by_time(KnownState, quantile1=25, quantile2=75)

			if Timeeffect == False:
				Qall = np.mean(Qall, axis=0)

			# find that day (both prediction and mins data)
			PredictionThatday = self.TimestampSignal[i:(i + stepsize)]
			print(PredictionThatday['Date'].iloc[0])
			PredictionThatday = PredictionThatday['Prediction'].copy().as_matrix()

			if Timeeffect == False:
				# if we will need to do anything that day
				PredictionThatday[PredictionThatday < Qall[0]] = -1
				PredictionThatday[PredictionThatday > Qall[1]] = 1
				PredictionThatday[(PredictionThatday > Qall[0]) & (PredictionThatday < Qall[1])] = 0
			else:
				# if we will need to do anything that day
				PredictionThatday[PredictionThatday < Qall[:, 0]] = -1
				PredictionThatday[PredictionThatday > Qall[:, 1]] = 1
				PredictionThatday[(PredictionThatday > Qall[:, 0]) & (PredictionThatday < Qall[:, 1])] = 0

			self.actionlist.append(PredictionThatday)

		self.actionlist = np.concatenate(self.actionlist)
		DATETIME = self.TimestampSignal[['DATETIME', 'Date', 'Time']][startpoint:(endpoint + stepsize)]
		DATETIME['Prediction'] = self.actionlist
		self.actionlist = DATETIME

	def crossday_fixedstoploss_mins_value_everyday(self, r, Type='LongShort'):
		"""
		to make decision every 30 mins and the dicided if we need to hold the position for another day
		r is how much percentage rate do we need to do stoploss
		Type is LongOnly ShortOnly LongShort
		"""
		self.datevalues = pd.DataFrame()

		# generate a actionlist of 1(buy,cover), 0(maintain the position before), -1(short,sell)
		try:
			actionlist = self.actionlist
		except:
			self.actionlist_generator(startn=13, Timeeffect=False)

			#保存预测信号
			actionlist = self.actionlist
			df_action = actionlist.copy()
			#actionlist.to_csv(r'data\pred_trade_signal.csv', index=None)

		while (len(actionlist) > 0):

			# find the first set of transaction: buy and then sell, or short and then cover
			if Type == 'LongShort':
				[actionindex1, actionindex2] = self.trading_index_finder(actionlist['Prediction'])

			if Type == 'LongOnly':
				[actionindex1, actionindex2] = self.trading_index_finder_longonly(actionlist['Prediction'])

			# that is the end of a loop, break out of the loop
			if ((actionindex1 == 'end') or (actionindex2 == 'end')):
				break

			[Datetime1, Action1] = actionlist[['DATETIME', 'Prediction']].iloc[actionindex1]
			[Datetime2, Action2] = actionlist[['DATETIME', 'Prediction']].iloc[actionindex2]

			# get the trading price
			DTtradep = self.TimestampPrice[(self.TimestampPrice['DATETIME'] >= Datetime1)&(self.TimestampPrice['DATETIME'] <= Datetime2)]
			DTtradep['close'] = DTtradep['close'].astype(float)

			if (Action1 == 1):
				if Type == 'LongOnly' or Type == 'LongShort':
					payoff = self.crossday_long_with_stoploss(r, DTtradep, self.opencost, self.closecost, self.closecost2)
					self.datevalues = self.datevalues.append(payoff[0])
				else:
					payoffvalue = pd.DataFrame([DTtradep['Date'].unique(), [0] * len(DTtradep['Date'].unique())]).transpose()
					payoffvalue.colnames = ['Date', 'Returns']
					payoff = [payoffvalue, DTtradep['DATETIME'].iloc[-1]]

			if (Action1 == (-1)):
				if Type == 'ShortOnly' or Type == 'LongShort':
					payoff = self.crossday_short_with_stoploss(r, DTtradep, self.opencost, self.closecost, self.closecost2)
					self.datevalues = self.datevalues.append(payoff[0])
				else:
					payoffvalue = pd.DataFrame([DTtradep['Date'].unique(), [0] * len(DTtradep['Date'].unique())]).transpose()
					payoffvalue.colnames = ['Date', 'Returns']
					payoff = [payoffvalue, DTtradep['DATETIME'].iloc[-1]]

				# update the actionlist
			actionlist = actionlist[actionlist['DATETIME'] > payoff[1]]
			print(payoff[1])

		return self.datevalues, df_action

